package cz.muni.fi.bugpatterndetector;

import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Method;
import org.apache.bcel.generic.ClassGen;
import org.apache.bcel.generic.Instruction;
import org.apache.bcel.generic.InstructionHandle;
import org.apache.bcel.generic.InstructionList;
import org.apache.bcel.generic.MethodGen;

import static cz.muni.fi.bugpatterndetector.InstructionParser.getJumpDestinationFromInstruction;
import static cz.muni.fi.bugpatterndetector.InstructionParser.getVariableFromInstruction;
import static org.apache.bcel.Repository.lookupClass;

/**
 *
 * @author Michal Ďuriš
 */
public class NullPointerBugPatternDetector implements BugPatternDetector {

    private static final String INVOKE_VIRTUAL_OPCODE = "invokevirtual";
    private static final String INVOKE_SPECIAL_OPCODE = "invokespecial";
    private static final String ALOAD_OPCODE = "aload";
    private static final String LDC_OPCODE = "ldc";
    private static final String ACONST_NULL_OPCODE = "aconst_null";
    private static final String ASTORE_OPCODE = "astore";
    private static final String IF_NON_NULL_OPCODE = "ifnonnull";
    private static final String IF_NULL_OPCODE = "ifnull";
    private static final String RETURN_OPCODE = "return";

    private static final int UNKNOWN_VALUE = -1;
    private static final int NULL_VALUE = 0;
    private static final int VARIOUS_VALUE = 1;

    private static final int JUMP_TO_END = -1;
    private static final int NO_JUMP = 0;

    private static final int RETURN = 1;
    private static final int NO_RETURN = 0;

    private boolean returnExecuted = false;

    private final List<Boolean> variablesNullDangerStatusList = new ArrayList<>(
            1);
    private final List<Integer> variablesValueList = new ArrayList<>(1);

    @Override
    public boolean methodContainsBugPattern(String fullyQualifiedClassName,
                                            String methodName) {

        try {
            JavaClass javaClass = lookupClass(fullyQualifiedClassName);
            ClassGen classGen = new ClassGen(javaClass);

            Method destinatedMethod = null;

            for (Method method : classGen.getMethods()) {
                if (method.getName().equals(methodName)) {
                    destinatedMethod = method;
                }
            }

            if (destinatedMethod == null) {
                throw new NoSuchMethodException();
            }

            MethodGen methodGen = new MethodGen(destinatedMethod, null, classGen
                    .getConstantPool());

            init(methodGen.getLocalVariables().length);

            return inspectMethod(methodGen.getInstructionList());

        } catch (ClassNotFoundException ex) {
            System.err.println("Class not found !");
        } catch (NoSuchMethodException ex) {
            System.err.println("Method not found !");
        }

        return false;
    }

    private void init(int localVariablesCount) {
        for (int i = 0; i < localVariablesCount; i++) {
            variablesNullDangerStatusList.add(i, true);
        }

        for (int i = 0; i < localVariablesCount; i++) {
            variablesValueList.add(i, UNKNOWN_VALUE);
        }
    }

    private void resetLists() {
        for (int i = 0; i < variablesNullDangerStatusList.size(); i++) {
            variablesNullDangerStatusList.set(i, true);
            variablesValueList.set(i, UNKNOWN_VALUE);
        }
    }

    private boolean inspectMethod(InstructionList instructionList) {

        boolean dangerMethod = false;

        for (int i = 0; i < instructionList.getInstructionHandles().length; i++) {

            InstructionHandle instructionHandle = instructionList
                    .getInstructionHandles()[i];

            if (instructionHandle.getInstruction().getName().trim().toLowerCase(
                    Locale.ENGLISH).startsWith(INVOKE_VIRTUAL_OPCODE)) {
                if (instructionHandle.getPrev().getInstruction().getName()
                        .trim().toLowerCase(Locale.ENGLISH).startsWith(
                                ALOAD_OPCODE)) {
                    if (inspectVariable(instructionList, i,
                            getVariableFromInstruction(instructionHandle
                                    .getPrev().getInstruction().toString(false),
                                    NullPointerBugPatternDetector.ALOAD_OPCODE))) {

                        dangerMethod = true;
                    }

                    resetLists();

                    if (returnExecuted) {

                        returnExecuted = false;
                        break;
                    }
                }
            }
        }

        return dangerMethod;
    }

    private boolean inspectVariable(InstructionList instructionList,
                                    int currentInstructionListIndex,
                                    int possibleDangerousVariable) {

        if (possibleDangerousVariable == -1) {
            throw new IllegalArgumentException();
        }

        int offset;

        for (int i = 0; i < currentInstructionListIndex; i = i + offset) {

            offset = 1;

            String instruction = instructionList.getInstructionHandles()[i]
                    .getInstruction().toString(false).trim().toLowerCase(
                            Locale.ENGLISH);

            if (instruction.startsWith(IF_NON_NULL_OPCODE) || instruction
                    .startsWith(IF_NULL_OPCODE)) {

                int jumpTargetPosition = inspectIf(instructionList
                        .getInstructionHandles()[i],
                        possibleDangerousVariable,
                        instructionList.getInstructionPositions()[currentInstructionListIndex]);

                if (jumpTargetPosition > 0) {

                    offset = calculateJumpOffset(instructionList
                            .getInstructionPositions(),
                            i,
                            jumpTargetPosition);

                    if (instructionList.getInstructionHandles()[i + offset - 1]
                            .getInstruction().getName().trim().toLowerCase(
                                    Locale.ENGLISH).contains(RETURN_OPCODE)) {
                        if (inspectReturn(instructionList
                                .getInstructionHandles()[i],
                                possibleDangerousVariable)) {

                            return false;
                        }
                    }

                } else if (jumpTargetPosition == JUMP_TO_END) {

                    break;
                }
            } else if (instruction.startsWith(ASTORE_OPCODE)) {

                inspectAstore(instructionList.getInstructionHandles()[i]);

            } else if (instruction.contains(RETURN_OPCODE)) {
                if (inspectReturn(getReturnContainingIfInstruction(
                        instructionList.getInstructionHandles()[i]),
                        possibleDangerousVariable)) {

                    returnExecuted = true;
                    return false;
                }
            }
        }

        return variablesNullDangerStatusList.get(possibleDangerousVariable);
    }

    private int inspectIf(InstructionHandle ifInstruction, int inspectedVariable,
                          int inspectedVariablePosition) {

        final String ifInstructionName = ifInstruction.getInstruction()
                .getName().trim().toLowerCase(Locale.ENGLISH);

        int ifTestedVariable = getVariableFromInstruction(
                ifInstruction.getPrev().getInstruction().toString(false).trim()
                .toLowerCase(Locale.ENGLISH),
                NullPointerBugPatternDetector.ALOAD_OPCODE);

        int jumpTargetPosition = NO_JUMP;

        if (isVariablePossibleDangerous(ifTestedVariable)) {
            if (isJumpingBehindInspectedVariable(inspectedVariablePosition,
                    ifInstruction.getInstruction())) {
                if (ifTestedVariable == inspectedVariable && ifInstructionName
                        .startsWith(IF_NULL_OPCODE)) {

                    variablesNullDangerStatusList.set(inspectedVariable, false);
                    variablesValueList.set(inspectedVariable, VARIOUS_VALUE);
                }
            } else {
                jumpTargetPosition = getJumpDestinationFromInstruction(
                        ifInstruction.getInstruction());
            }
        } else {

            if ((variablesValueList.get(ifTestedVariable) == NULL_VALUE && ifInstructionName
                 .startsWith(IF_NULL_OPCODE))
                || (variablesValueList.get(ifTestedVariable) == VARIOUS_VALUE && ifInstructionName
                    .startsWith(IF_NON_NULL_OPCODE))) {

                if (isJumpingBehindInspectedVariable(inspectedVariablePosition,
                        ifInstruction.getInstruction())) {

                    variablesNullDangerStatusList.set(inspectedVariable, false);
                    jumpTargetPosition = JUMP_TO_END;

                } else {

                    jumpTargetPosition = getJumpDestinationFromInstruction(
                            ifInstruction
                            .getInstruction());
                }
            }
        }

        return jumpTargetPosition;
    }

    private void inspectAstore(InstructionHandle astoreInstruction) {

        int astoreDestinationVariable = getVariableFromInstruction(
                astoreInstruction.getInstruction().toString(false),
                NullPointerBugPatternDetector.ASTORE_OPCODE);

        String sourceToStoreInstruction = astoreInstruction.getPrev()
                .getInstruction().toString(false).trim().toLowerCase(
                        Locale.ENGLISH);

        if (sourceToStoreInstruction.startsWith(ALOAD_OPCODE)) {

            int aloadInstructionVariable = getVariableFromInstruction(
                    sourceToStoreInstruction,
                    NullPointerBugPatternDetector.ALOAD_OPCODE);

            variablesNullDangerStatusList.set(astoreDestinationVariable,
                    variablesNullDangerStatusList.get(aloadInstructionVariable));

            variablesValueList.set(astoreDestinationVariable, variablesValueList
                    .get(aloadInstructionVariable));

        } else if (sourceToStoreInstruction.startsWith(ACONST_NULL_OPCODE)) {

            variablesNullDangerStatusList.set(astoreDestinationVariable, true);
            variablesValueList.set(astoreDestinationVariable, NULL_VALUE);

        } else if (sourceToStoreInstruction.startsWith(INVOKE_VIRTUAL_OPCODE) || sourceToStoreInstruction
                .startsWith(INVOKE_SPECIAL_OPCODE)) {

            variablesNullDangerStatusList.set(astoreDestinationVariable, true);
            variablesValueList.set(astoreDestinationVariable, UNKNOWN_VALUE);

        } else if (sourceToStoreInstruction.startsWith(LDC_OPCODE)) {

            variablesNullDangerStatusList.set(astoreDestinationVariable, false);
            variablesValueList.set(astoreDestinationVariable, VARIOUS_VALUE);

        }
    }

    private boolean inspectReturn(InstructionHandle ifInstruction,
                                  int possibleDangerousVariable) {

        String instructionName = ifInstruction.getInstruction().getName().trim()
                .toLowerCase(Locale.ENGLISH);

        int ifTestedVariable = getVariableFromInstruction(
                ifInstruction.getPrev().getInstruction().toString(false),
                NullPointerBugPatternDetector.ALOAD_OPCODE);

        if (isVariablePossibleDangerous(ifTestedVariable)) {

            if (possibleDangerousVariable == ifTestedVariable && instructionName
                    .startsWith(IF_NON_NULL_OPCODE)) {

                return true;
            }

        } else {

            if ((variablesValueList.get(ifTestedVariable) == NULL_VALUE && instructionName
                 .startsWith(IF_NON_NULL_OPCODE))
                || (variablesValueList.get(ifTestedVariable) == VARIOUS_VALUE && instructionName
                    .startsWith(IF_NULL_OPCODE))) {

                return true;
            }
        }

        return false;
    }

    private InstructionHandle getReturnContainingIfInstruction(
            InstructionHandle returnInstruction) {

        InstructionHandle ifInstructionHandle = returnInstruction.getPrev();
        String instruction;

        boolean searching = true;

        do {
            ifInstructionHandle = ifInstructionHandle.getPrev();

            if (ifInstructionHandle != null) {
                
                instruction = ifInstructionHandle.getInstruction().getName()
                        .trim().toLowerCase(Locale.ENGLISH);

                if (instruction.startsWith(IF_NULL_OPCODE) || instruction
                        .startsWith(IF_NON_NULL_OPCODE)) {
                    
                    searching = false;
                }
                
            } else {
                
                searching = false;
            }

        } while (searching);

        return ifInstructionHandle;
    }

    private boolean isVariablePossibleDangerous(int variableID) {
        
        return variablesNullDangerStatusList.get(variableID) && variablesValueList
                .get(variableID) == UNKNOWN_VALUE;
    }

    private static boolean isJumpingBehindInspectedVariable(
            int inspectedVariablePosition, Instruction ifInstruction) {
        
        return getJumpDestinationFromInstruction(ifInstruction) > inspectedVariablePosition;
    }

    private static int calculateJumpOffset(int[] instructionPositions,
                                           int currentInstructionPositionIndex,
                                           int targetPosition) {
        
        int offsetPositionIndex = currentInstructionPositionIndex + 1;

        for (; offsetPositionIndex < instructionPositions.length;
                offsetPositionIndex++) {
            
            if (instructionPositions[offsetPositionIndex] == targetPosition) {
                break;
            }
        }

        return offsetPositionIndex - currentInstructionPositionIndex;
    }

    public static void main(String[] args) {
        
        if (args.length != 2) {
            System.out.println(
                    "Two params needed: <fully qualified class name> "
                    + "<method name>");
            return;
        }

        NullPointerBugPatternDetector nullPointerBugPatternDetector = new NullPointerBugPatternDetector();

        if (nullPointerBugPatternDetector.methodContainsBugPattern(args[0],
                args[1])) {
            System.out.println("Method is null pointer dangerous ! :( ");
        } else {
            System.out.println("Method is null pointer safe. :) ");
        }
    }
}
